﻿using System;

namespace GeoCode.Core
{
    /// <summary>
    /// 
    /// </summary>
    /// <remarks></remarks>
    public struct Location
    {
        /// <summary>
        /// 
        /// </summary>
        public static readonly Location Empty;

        /// <summary>
        /// 
        /// </summary>
        private readonly double _latitude;
        /// <summary>
        /// 
        /// </summary>
        private readonly double _longitude;

        /// <summary>
        /// Initializes a new instance of the <see cref="Location"/> struct.
        /// </summary>
        /// <param name="latitude">The latitude.</param>
        /// <param name="longitude">The longitude.</param>
        /// <remarks></remarks>
        public Location(double latitude, double longitude)
        {
            _latitude = latitude;
            _longitude = longitude;
        }

        /// <summary>
        /// Gets the latitude.
        /// </summary>
        /// <remarks></remarks>
        public double Latitude
        {
            get { return _latitude; }
        }

        /// <summary>
        /// Gets the longitude.
        /// </summary>
        /// <remarks></remarks>
        public double Longitude
        {
            get { return _longitude; }
        }

        /// <summary>
        /// Toes the radian.
        /// </summary>
        /// <param name="val">The val.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        private static double ToRadian(double val)
        {
            return (Math.PI/180.0)*val;
        }

        /// <summary>
        /// Distances the between.
        /// </summary>
        /// <param name="location">The location.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public Distance DistanceBetween(Location location)
        {
            return DistanceBetween(location, DistanceUnits.Miles);
        }

        /// <summary>
        /// Distances the between.
        /// </summary>
        /// <param name="location">The location.</param>
        /// <param name="units">The units.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public Distance DistanceBetween(Location location, DistanceUnits units)
        {
            double earthRadius = (units == DistanceUnits.Miles)
                                     ? Distance.EarthRadiusInMiles
                                     : Distance.EarthRadiusInKilometers;

            double latRadian = ToRadian(location.Latitude - Latitude);
            double longRadian = ToRadian(location.Longitude - Longitude);

            double a = Math.Pow(Math.Sin(latRadian/2.0), 2) +
                       Math.Cos(ToRadian(Latitude))*
                       Math.Cos(ToRadian(location.Latitude))*
                       Math.Pow(Math.Sin(longRadian/2.0), 2);

            double c = 2.0*Math.Asin(Math.Min(1, Math.Sqrt(a)));

            double distance = earthRadius*c;
            return new Distance(distance, units);
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
        /// <remarks></remarks>
        public override int GetHashCode()
        {
            return _latitude.GetHashCode() ^ _longitude.GetHashCode();
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">Another object to compare to.</param>
        /// <returns><c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.</returns>
        /// <remarks></remarks>
        public override bool Equals(object obj)
        {
            if (!(obj is Location))
                return false;

            return Equals((Location)obj);
        }

        /// <summary>
        /// Equalses the specified other.
        /// </summary>
        /// <param name="other">The other.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public bool Equals(Location other)
        {
            if (_latitude != other._latitude)
                return false;

            return _longitude == other._longitude;
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="point1">The point1.</param>
        /// <param name="point2">The point2.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static bool operator ==(Location point1, Location point2)
        {
            return point1.Equals(point2);
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="point1">The point1.</param>
        /// <param name="point2">The point2.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static bool operator !=(Location point1, Location point2)
        {
            return !point1.Equals(point2);
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>A <see cref="System.String"/> that represents this instance.</returns>
        /// <remarks></remarks>
        public override string ToString()
        {
            return String.Format("{0}, {1}", _latitude, _longitude);
        }
    }
}